package jam4j.lang;

import java.io.IOException;
import java.util.List;

import jam4j.OutputChannel;
import jam4j.build.Target;
import jam4j.util.Globs;
import jam4j.util.Strings;

abstract class Statement {
    /**
     * Execute the statement in the given context. The statement may use or
     * modify the context, and it may either end normally and allow execution
     * to proceed to the next statement or throw a {@link Jump}.
     * 
     * @param cxt The context in which to run the statement.
     * @throws Jump To perform a jump in execution.
     * 
     * @see Context
     * @see Jump
     */
    abstract void run(Context cxt) throws Jump;
    
    /**
     * Special checked throwable used for flow control. The break, continue,
     * and return statements are implemented simply by throwing a
     * {@link BreakJump}, {@link ContinueJump}, or {@link ReturnJump} object, 
     * respectively, and these are all subclasses of Jump.
     * 
     * @author Luke Maurer
     */
    private static abstract class Jump extends Throwable {  
        Jump() { }
        
        // We don't need no steenkin' stack trace ...
        public final Throwable fillInStackTrace() { return this; }
    }
    
    private static final class BreakJump extends Jump {
        BreakJump() { }
        public String toString() { return "BreakJump"; }
    }
    private static final class ContinueJump extends Jump {
        ContinueJump() { }
        public String toString() { return "ContinueJump"; }
    }
    private static final class ReturnJump extends Jump {
        final String[] strings;
        ReturnJump(String[] strings) { this.strings = strings; }
        public String toString() {
            return "ReturnJump [" + Strings.join(" ", strings) + "]"; 
        }
    }
    
    /**
     * Run the statement, as if it is a rule body. Accordingly, any break or
     * continue statement within the statement will be an error, and any return
     * statement will exit early.
     * 
     * @param cxt The context in which to run the statement.
     * @return The value returned by a return statement, if one is encountered,
     *         or an empty string array otherwise.
     */
    final String[] runRuleBody(Context cxt) {
        try {
            this.run(cxt);
            return Expression.UNDEF;
        } catch (ReturnJump e) {
            return e.strings;
        } catch (BreakJump e) {
            throw new RuntimeException("break without loop");
        } catch (ContinueJump e) {
            throw new RuntimeException("continue without loop");
        } catch (Jump e) {
            throw new RuntimeException("unknown jump type", e);
        }
    }
    
    private static final class Actions extends Statement {
        private final Expression name;
        private final Expression body;
        private final int maxLineLength;
        private final String[] boundVariables;
        private final ActionDef.Modifier[] modifiers;
        
        Actions(Expression name, Expression body, int maxLineLength, 
                String[] boundVariables,
                ActionDef.Modifier ... modifiers) {
            this.name = name;
            this.body = body;
            this.maxLineLength = maxLineLength;
            this.boundVariables = boundVariables;
            this.modifiers = modifiers;
        }
        
        void run(Context cxt) {
            for (String ruleName : name.resolve(cxt))
                cxt.rule(ruleName).defineAction(
                        body, maxLineLength, boundVariables, modifiers);
        }
        
        @Override
        public String toString() {
            final StringBuilder builder =
                new StringBuilder("Actions [rule=")
                    .append(name)
                    .append(" modifiers=[");
            
            Strings.join(builder, " ", modifiers)
                .append(']');
            if (boundVariables.length > 0) {
                builder.append(" bind=[");
                Strings.join(builder, " ", boundVariables)
                    .append(']');
            }
            
            return builder.append(']').toString();
        }
    }
    
    enum Operation {
        ASSIGN("="),
        APPEND("+="),
        DEFAULT("?="),
        ;
        
        final String operator;
        
        private Operation(String operator) {
            this.operator = operator;
        }
        
        static Operation byTokenType(int tokenType) {
//            switch (tokenType) {
//                case Jam4JParser.EQUAL:
//                    return ASSIGN;
//                case Jam4JParser.PLUS_EQUAL:
//                    return APPEND;
//                case Jam4JParser.QUESTION_EQUAL:
//                    return DEFAULT;
//                default:
//                    throw new IllegalArgumentException(
//                            "Not an assignment operator: " +
//                            Jam4JParser.tokenNames[tokenType]);
//            }
            return null;
        }
    }
    
    private static final class Assignment extends Statement {
        private final Operation operation;
        private final Expression targetNames;
        private final Expression vars;
        private final Expression value;
        
        Assignment(Operation operation, Expression vars, Expression value) {
            this(operation, null, vars, value);
        }
        
        Assignment(Operation operation, 
                Expression targets, Expression vars, Expression value) {
            this.operation = operation;
            this.targetNames = targets;
            this.vars = vars;
            this.value = value;
        }
        
        void run(Context cxt) {
            if (targetNames == null)
                logExec("set %s %s %s\n", vars, operation.operator, value);
            else
                logExec("set %s on %s %s %s\n",
                        vars, targetNames, operation.operator, value);
            
            final Target[] targets;
            {
                if (targetNames == null)
                    targets = new Target[] { null };
                else {
                    final String[] targetNameValues = targetNames.resolve(cxt);
                    final int count = targetNameValues.length;
                    targets = new Target[count];
                    for (int ix = 0; ix < count; ix++)
                        targets[ix] = cxt.target(targetNameValues[ix]);
                }
            }
            
            final String[] varValues = vars.resolve(cxt);
            final String[] valueValues = value.resolve(cxt);
            
            for (Target target : targets)
                for (String var : varValues)
                    switch (operation) {
                        case ASSIGN:
                            cxt.set(target, var, valueValues);
                            break;
                        case APPEND:
                            cxt.setAppend(target, var, valueValues);
                            break;
                        case DEFAULT:
                            cxt.setDefault(target, var, valueValues);
                            break;
                        default:
                            throw new AssertionError(
                                    "Unknown operation " + operation);
                    }
                
        }
        
        public String toString() {
            final StringBuilder builder =
                new StringBuilder().append(vars).append(' ');
            
            if (targetNames != null)
                builder.append("on ").append(targetNames).append(' ');
            
            return builder.append(operation.operator)
                .append(' ').append(value)
                .toString();
        }
    }

    private static final class Block extends Statement {
        private final Statement[] stmts;
        
        Block(Statement ... stmts) {
            this.stmts = stmts;
        }
        
        void run(Context cxt) throws Jump {
            cxt.enterScope();
            try {
                for (Statement stmt : stmts)
                    stmt.run(cxt);
            } finally {
                cxt.leaveScope();
            }
        }
        
        public String toString() {
            final StringBuilder builder = new StringBuilder("Block {");
            Strings.join(builder, ", ", stmts);
            return builder.append('}').toString();
        }
    }
    
    private static final class Break extends Statement {
        private static final BreakJump BREAK = new BreakJump();
        
        void run(Context cxt) throws BreakJump {
            throw BREAK;
        }
        
        public String toString() {
            return "Break";
        }
    }

    private static final class Call extends Statement {
        private final Expression name;
        private final Expression[] args;
        
        public Call(Expression name, Expression ... args) {
            this.name = name;
            this.args = args;
        }
        
        void run(Context cxt) {
            for (String name : this.name.resolve(cxt))
                cxt.call(name, null, cxt.values(args));
        }
        
        @Override
        public String toString() {
            final StringBuilder builder =
                new StringBuilder("Call [rule=")
                    .append(name).append(" args=(");
            
            Strings.join(builder, ", ", args);
            return builder.append(")]").toString();
        }
    }
    
    private static final class Continue extends Statement {
        private static final ContinueJump CONTINUE = new ContinueJump();
        
        void run(Context cxt) throws ContinueJump {
            throw CONTINUE;
        }
        
        @Override
        public String toString() {
            return "Continue";
        }
    }
    
    private static final class For extends Statement {
        private final String var;
        private final Expression list;
        private final Statement stmt;
        
        For(String var, Expression list, Statement stmt) {
            this.var = var;
            this.list = list;
            this.stmt = stmt;
        }
        
        void run(Context cxt) throws Jump {
            cxt.enterScope();
            try {
                cxt.makeLocal(var);
                for (String value : list.resolve(cxt)) {
                    cxt.set(var, value);
                    try {
                        stmt.run(cxt);
                    } catch (ContinueJump e) {
                        continue;
                    } catch (BreakJump e) {
                        break;
                    }
                }
            } finally {
                cxt.leaveScope();
            }   
        }
        
        @Override
        public String toString() {
            return new StringBuilder("For [var=")
                .append(var).append(" list=")
                .append(list).append(" { ")
                .append(stmt).append(" }]")
                .toString();
        }
    }
    
    private static final class If extends Statement {
        private final Expression.Boolean expr;
        private final Statement stmt, alt;
        
        If(Expression.Boolean expr, Statement stmt, Statement alt) {
            this.expr = expr;
            this.stmt = stmt;
            this.alt = alt;
        }
        
        If(Expression.Boolean expr, Statement stmt) {
            this(expr, stmt, null);
        }
        
        void run(Context cxt) throws Jump {
            if (expr.isTrue(cxt))
                stmt.run(cxt);
            else if (alt != null)
                alt.run(cxt);
        }
        
        @Override
        public String toString() {
            final StringBuilder builder =
                new StringBuilder("If ")
                    .append(expr).append(" { ")
                    .append(stmt).append(" }");
            
            return (alt == null ? 
                    builder : builder.append(" Else { ").append(alt).append(" }"))
                        .append(']').toString();
        }
    }
    
    private static final class Include extends Statement {
        private final Expression targetExpression;

        Include(Expression targetExpression) {
            this.targetExpression = targetExpression;
        }
        
        void run(Context cxt) {
            final String targetName;
            {
                final String[] names = targetExpression.resolve(cxt);
                if (names.length == 0)
                    return;
                targetName = names[0];
            }
            
            logExec("include %s \n", targetName);
            final Target target = cxt.target(targetName);
            if (target.bind(cxt).updateTime().getTime() != 0 ||
                    !target.flags().contains(Target.Flag.NOCARE))
                try {
                    final String filename =
                        FilePath.absoluteFilename(target.boundName());
                    final JamFile jamFile = JamFile.parse(filename);
                    jamFile.run(cxt);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                } catch (ParseException e) {
                    throw new RuntimeException(e);
                }
        }
        
        @Override
        public String toString() {
            return "Include(" + targetExpression + ')';
        }
    }
    
    private static final class Local extends Statement {
        private final Expression names, value;
        
        Local(Expression names, Expression value) {
            this.names = names;
            this.value = value;
        }
        
        void run(Context cxt) {
            logExec("local %s = %s\n", names, value);
            
            final String[] resolvedValue =
                value == null ? null : value.resolve(cxt);
            
            for (String name : names.resolve(cxt))
                cxt.setLocal(name, resolvedValue);
        }
        
        @Override
        public String toString() {
            return "Local(" + names + ", " + value + ')';
        }
    }

    private static final class On extends Statement {
        private final Expression target;
        private final Statement stmt;
        
        On(Expression target, Statement stmt) {
            this.target = target;
            this.stmt = stmt;
        }

        void run(Context cxt) throws Jump {
            logExec("on %s\n", target);
            
            final Target target;
            {
                final String[] targetNames = this.target.resolve(cxt);
                if (targetNames.length == 0)
                    // This is what jam does ...
                    return;
                
                target = cxt.target(targetNames[0]);
            }
            
            cxt.enterOnTarget(target);
            try {
                stmt.run(cxt);
            } finally {
                cxt.leaveScope();
            }
        }
        
        @Override
        public String toString() {
            return "On [target=" + target + " { " + stmt + " }]";
        }
    }
    
    private static final class Return extends Statement {
        private final Expression value;
        Return(Expression value) {
            this.value = value;
        }
        
        void run(Context cxt) throws ReturnJump {
            throw new ReturnJump(value.resolve(cxt));
        }
        
        @Override
        public String toString() {
            return "Return(" + value + ')';
        }
    }
    

    private static final class Rule extends Statement {
        private final String name;
        private final String[] argNames;
        private final Statement body;
        
        Rule(String name, String[] args, Statement body) {
            this.name = name;
            this.argNames = args;
            this.body = body;
        }

        void run(Context cxt) throws Jump {
            logExec("rule %s %s\n", name,
                    Strings.rightJoiner(" ", (Object[]) argNames));
            
            cxt.rule(name).setProcedure(
                new UserProcedure(name, argNames, body));
        }
        
        @Override
        public String toString() {
            return "Rule(" + name + ')';
        }
    }
    
    static final class Case {
        private final String glob;
        private final Statement[] stmts;
        
        Case(String glob, Statement ... stmts) {
            this.glob = glob;
            this.stmts = stmts;
        }
        
        @Override
        public String toString() {
            return Strings.join(
                    new StringBuilder("Case [glob=")
                            .append(glob).append(" { "),
                        ", ", stmts)
                .append(" }]").toString();
        }
    }
    
    private static final class Switch extends Statement {
        private final Expression scrutinee;
        private final Case[] cases;
        
        Switch(Expression scrutinee, Case ... cases) {
            this.scrutinee = scrutinee;
            this.cases = cases;
        }
        
        void run(Context cxt) throws Jump {
            logExec("switch %s\n", scrutinee);
            
            final String value;
            {
                final String[] values = scrutinee.resolve(cxt);
                if (values.length == 0)
                    return;
                value = values[0];
            }
            
            for (Case caze : cases) {
                if (Globs.matches(value, caze.glob)) {
                    for (Statement stmt : caze.stmts)
                        stmt.run(cxt);
                    break;
                }
            }
        }
        
        @Override
        public String toString() {
            final StringBuilder builder = new StringBuilder("Switch { ");
            Strings.join(builder, ", ", cases);
            return builder.append(" }").toString();
        }
    }

    private static final class While extends Statement {
        private final Expression.Boolean expr;
        private final Statement stmt;
        
        While(Expression.Boolean expr, Statement stmt) {
            this.expr = expr;
            this.stmt = stmt;
        }
        
        void run(Context cxt) throws Jump {
            while (expr.isTrue(cxt))
                try {
                    stmt.run(cxt);
                } catch (BreakJump e) {
                    break;
                } catch (ContinueJump e) {
                    continue;
                }
        }
        
        @Override
        public String toString() {
            return "While " + expr + " { " + stmt + " }";
        }
    }

    private static final ThreadLocal<Integer> CALL_STACK_DEPTH =
        new ThreadLocal<Integer>() {
            protected Integer initialValue() {
                return 0;
            }
        };
    
    static int callStackDepth() {
        return CALL_STACK_DEPTH.get();
    }
    
    static void increaseStackDepth() {
        CALL_STACK_DEPTH.set(callStackDepth() + 1);
    }
    
    static void decreaseStackDepth() {
        final int depth = callStackDepth();
        if (depth == 0)
            throw new IllegalStateException("Call stack is empty");
        CALL_STACK_DEPTH.set(depth - 1);
    }
    
    static void logExec(String format, Object ... args) {
        if (!OutputChannel.COMPILE.isEnabled())
            return;
        
        final StringBuilder indentedFormatBuilder = new StringBuilder();
        indentedFormatBuilder.append(">>>>|>>>>|>>>>|>>>>|>>>>|>>>>|>>>>|",
                    0, ((1 + callStackDepth()) * 2) % 35)
                .append(' ').append(format);
        
        OutputChannel.COMPILE.printf(indentedFormatBuilder.toString(), args);
    }
    
    static Statement actionDefs(
            Expression name, Expression body, int maxLineLength,
            String[] boundVariables,
            ActionDef.Modifier ... modifiers) {
        return new Actions(name, body, maxLineLength, 
                boundVariables, modifiers);
    }
    
    static Statement actionDefs(
            Expression name, Expression body, int maxLineLength,
            List<String> boundVariables,
            List<ActionDef.Modifier> modifiers) {
        return actionDefs(name, body, maxLineLength,
                boundVariables.toArray(new String[boundVariables.size()]),
                modifiers.toArray(new ActionDef.Modifier[modifiers.size()]));
    }
    
    static Statement assignment(
            Operation op, Expression targets, Expression vars, Expression value) {
        return new Assignment(op, targets, vars, value);
    }
    
    static Statement block(Statement ... stmts) {
        if (stmts.length == 1) {
            // We don't need to wrap the statement in a one-statement block -
            // unless, that is, it's a local variable declaration, in which
            // case keeping it unwrapped changes the (admittedly odd) semantics
            // of the program.
            
            final Statement stmt;
            if (!((stmt = stmts[0]) instanceof Local))
                return stmt;
        }
        return new Block(stmts);
    }
    
    static Statement block(List<Statement> stmts) {
        return block(stmts.toArray(new Statement[stmts.size()]));
    }
    
    static Statement break_() {
        return new Break();
    }
    
    static Statement call(Expression name, Expression ... args) {
        return new Call(name, args);
    }
    
    static Statement call(Expression name, List<Expression> args) {
        return call(name, args.toArray(new Expression[args.size()]));
    }
    
    static Statement continue_() {
        return new Continue();
    }
    
    static Statement for_(String var, Expression list, Statement stmt) {
        return new For(var, list, stmt);
    }
    
    static Statement if_(
            Expression.Boolean cond, Statement cons, Statement alt) {
        return new If(cond, cons, alt);
    }
    
    static Statement include(Expression value) {
        return new Include(value);
    }
    
    static Statement local(Expression names, Expression value) {
        return new Local(names, value);
    }
    
    static Statement on(Expression target, Statement stmt) {
        return new On(target, stmt);
    }
    
    static Statement return_(Expression value) {
        return new Return(value);
    }
    
    static Statement rule(String name, String[] argNames, Statement body) {
        return new Rule(name, argNames, body);
    }
    
    static Statement rule(String name, List<String> argNames, Statement body) {
        return rule(name, argNames.toArray(new String[argNames.size()]), body);
    }
    
    static Statement switch_(Expression scrutinee, Case ... cases) {
        return new Switch(scrutinee, cases);
    }
    
    static Statement switch_(Expression scrutinee, List<Case> cases) {
        return switch_(scrutinee, cases.toArray(new Case[cases.size()]));
    }
    
    static Statement while_(Expression.Boolean cond, Statement stmt) {
        return new While(cond, stmt);
    }
}
